#!/usr/bin/env python 
# coding: utf8

""" 
Defines the Problem, Solution, Criterion, and Response Record classes. 

This file is part of PeerTool, a web application for collaborative
decision-making (Copyrighted, 2009-2010, Ellis & Grant, Inc.)
Developed by Michael F. Ellis <michael.f.ellis@gmail.com>.  
License: AGPL v3
PeerTool comes with ABSOLUTELY NO WARRANTY.
""" 
try:
    ## Web2py framework imports
    from gluon.html import *
    from gluon.http import *
    from gluon.validators import *
    from gluon.sqlhtml import *
except ImportError:
    pass    
import applogger
debug = applogger.consoleLogger("peertool").debug  

import uuid    
import time
import sys
import re
from copy import deepcopy
from operator import itemgetter

## Web2py framework requirements
# request, response, session, cache, T, db(s) 
# must be passed and cannot be imported!
##
def hist(values,nbins=9,twosided=True):
    ## Utility used to generate histogram lists
    ## of solution and criteria responses.
    if twosided:
        scale = nbins/2
        offset = scale
    else:
        scale = nbins-1
        offset = 0   
    bins = [0]*nbins
    values.sort()
    for v in values:
        bins[int(round(scale*v))+offset] += 1
        
    return bins
    
    
def ranking(d):
    """
    Utility used to rank solution and criteria responses.  d is a dict whose
    values are lists of numeric values, e.g. {'a':[0.1, -.5, .3], 'b':[.7,
    -.34], ...}. The function returns a list of key, value pairs sorted in
    descending value order.  The values are sum(listvalues)/len(listvalues).
    """
    r = dict()
    for k,vlist in d.iteritems():
        r[k] = float(sum(vlist))/len(vlist) if len(vlist) > 0 else 0.0
    return sorted(r.iteritems(), key=itemgetter(1), reverse=True)    

_striptags = re.compile(r'<[^<]*?/?>')
_nbsp = re.compile(r'&nbsp;')

def isEmptyHtml(text):
    """ 
    Checks to see if text is empty after removing HTML tags and
    leading/trailing whitespace incl. newlines.
    """
    t  =  _striptags.sub('',text) 
    return 0 == len(_nbsp.sub('',t).strip())
    
def mkLabel(text,nchars=64):
    """ 
    Returns as many complete words from first non-blank line of text as will
    fit into nchars.  If first word is longer than nchars, it is truncated to
    nchars-3 and returned with 3 dots appended.
        
    """
    assert nchars > 0
    lines = text.split('\n')
    for line in lines:
        cleaned = _striptags.sub('',line).strip()
        sp = cleaned.split()                        ## split on whitespace
        if len(sp) > 0:
            if len(sp[0])>nchars:                   ## first won't fit, 
                                                    ## so truncate
                return sp[0][0:nchars-3] + "..."
            else:
                n = 0
                for i,w in enumerate(sp):           ## return as many as 
                                                    ## will fit
                    n = i+1
                    label = " ".join(sp[0:n])
                    if len(label) > nchars:
                        n -= 1
                        break
                return " ".join(sp[0:n])

    ## Bad news if we get to here.
    raise ValueError("Empty text!")        

class Comment(object):
    def __init__(self,who,text):
        self.who=who
        self.text=text

def addComment(obj,who,text):
    oc = obj.comments
    if len(oc) == 0:
        oc.append(dict())
    oc[0][time.time()] = Comment(who,text)

def getComments(obj):
    oc = obj.comments
    if len(oc) == 0:
        oc.append(dict())
    return oc[0]


class Solution(object):
    """ Represents a possible solution to a problem and holds responses by team
    members 
    """
    def __init__(self,text):
        self.text = None
        self.updateText(text)           # description of the solution
        self.responses = dict()         # place to hold responses from 
                                        # team members
        self.deletionvotes = dict()
        self.active = True              # Allows solutions to be "deactivated"
        self.uuid = uuid.uuid1() 
        self.comments = [{}]            # List with one entry: a dict of 
                                        # Comment objects

    def updateText(self,newtext):
        if isEmptyHtml(newtext):
            return  ## prevent empty items
        if self.text != newtext:
            self.text = newtext
            self.lastmodified = time.time() # used to detect that the solution
                                            # description has changed.
        
    def addResponse(self,teammemberid, response=([0,0.0])):
        """ 
        A Solution response is a team member's evaluation of a particular
        solution. It is a tuple containing one or more two-element lists.  Each
        list has as its first member an integer criterion id and as its second
        member a float between -1 and +1 inclusive.  The second member
        represents the team members assessment of the solution in terms of the
        criterion on a scale ranging from "thumbs down" to "thumbs up".
        Example:  
            ([0,0.0],[1,-.75],[2,1.0]) means the team member thinks the
            solution is:
            * neutral with respect to criterion 0
            * strongly negative with respect to criterion 1
            * very strongly positive with respect to criterion 3
        """
        assert len(response) > 0
        self.responses[teammemberid] = dict()
        for pair in response:
            cid,thumb = pair
            #assert(isinstance(cid, int) and 0 <= cid) #criterion id
            assert(isinstance(thumb,float) and -1.0 <= thumb <=  1.0)
            self.responses[teammemberid][cid] = float(thumb)
            
    def getResponse(self,teammemberid):
        try:
            return self.responses[teammemberid]
        except KeyError:
            return None        

class Criterion(object):
    """ 
    Represents a criterion for evaluating solutions to a problem and holds team
    member responses. 
    """
    def __init__(self,text):
        self.text = None
        self.updateText(text)
        self.responses = dict()
        self.normalizedresponses = dict()
        self.deletionvotes = dict()
        self.uuid = uuid.uuid1()  
        self.comments = [{}]                    # List of user comments
              
    def updateText(self,newtext):
        if isEmptyHtml(newtext):
            return                              # prevent empty items
        if self.text != newtext:
            self.text = newtext
            self.lastmodified = time.time()     # used to detect that the 
                                                # solution description has 
                                                # changed.
    
    def normalizeResponse(self,teammemberid, sum, ncriteria):
        """
        Typically called by Problem.computeNormalizedCriteria().  sum is the
        total of all the teammemberid's criteria responses ncriteria is the
        number of criteria in the problem.
        """
        assert 0.0 <= sum <= ncriteria
        assert isinstance(ncriteria, int) and ncriteria > 0
        if sum > 0.0:
            self.normalizedresponses[teammemberid] = self.responses[teammemberid]/sum
        else:
            self.normalizedresponses[teammemberid] = 1.0/ncriteria    
            
    def addResponse(self,teammemberid, value=0.0):
        """
        A Criterion response consists of a single float between 0 and 1
        inclusive. It represents a team members assessment of the weight to be
        assigned to the criterion.  The scale is interpreted as a continuum,
        e.g.:
            * 0.0 -> no weight
            * 0.25 -> slight weight
            * 0.50 -> moderate weight
            * 0.75 -> heavy weight
            * 1.0 -> very heavy weight
            
        Note that a normalization is applied before a team member's criteria
        weights are applied to solution responses. Each criteria response is
        divided by the sum of all that member's criteria responses to enforce a
        constraint that each member has the same contribution to the overall
        outcome.    
        """
        assert(isinstance(value, float) and 0.0 <= value <=  1.0)
        self.responses[teammemberid] = float(value)
        self.normalizedresponses[teammemberid] = 0.0                       

    def getResponse(self,teammemberid):
        try:
            return self.responses[teammemberid]
        except KeyError:
            return None      
    

class Problem(object):
    """ Defines the essential parts of a problem to be solved:
            Problem Description
            Possible Solutions
            Criteria For Evaluating Solutions
            Team Members (as integer id's)
            Latest Responses from Team Members
         """
    def __init__(self, text, creatorid, 
        itemdeletionthreshold=0.75,             # Fraction of team members 
                                                # who must approve deletion 
                                                # of any solution or criterion
        defaultsolutiontext = "<p>Status Quo</p>",
        defaultcriteriontext = "<p>All (other) criteria</p>",):

        self.mkuuid()                           # Unique for each instance
        self.text = None                        # User's description of 
                                                # the problem.
        self.updateText(text)
        self.solutions = dict()                 # Dict of Solution objects
                                                # one for each proposed 
                                                # solution.    
        self.criteria = dict()                  # Dict of criteria objects.
        self.nextsolutionid = 0                 # Incrementing int to ensure 
                                                # unique keys.
        self.nextcriterionid = 0                # Similar for criteria.
        self.lastresponses = dict()             # {id:time_of_last_response}
                                                # Also serves as list of
                                                # team member ids.
        
        # Start life with one solution, one criterion 
        # and one team member.
        defaultsolution = Solution(defaultsolutiontext)
        self.addSolution(defaultsolution)
        defaultcriterion = Criterion(defaultcriteriontext)
        self.addCriterion(defaultcriterion)
        self.addTeamMember(creatorid)

        self.creatorid = creatorid              # DEPRECATED: Don't write 
                                                # new code the uses it. 

        self.srankings = []                     # Solution favorability 
                                                # rankings. list of (id,
                                                # value) pairs sorted by value.

        self.crankings = []                     # criteria weigth rankings.
        self.scrankings = dict()                # per-criteria rankings 
                                                # for each solution
        self.lastmodified = time.time()
        self.comments = [{},{}]                 # Dict of user comments 
                                                # in element 0.  Dict of 
                                                # synomyms in element 1. 
                    
        self.itemdeletionthreshold = itemdeletionthreshold

    def isAbandoned(self):
        """
        A problem is abandoned when there are no more participants.
        """
        return 0 == len(self.lastresponses)

    def getSynonyms(self):
        """ 
        Returns a dictionary containing preferred synonyms for singular and
        plural forms of 'problem', 'solution' and 'criterion'.
        """ 
        if len(self.comments) > 1:
            return self.comments[1];            # silly location, but who 
                                                # cares?
        else:
            return None

    def setSynonyms(self,d): 
        """ Replaces problem synonym dictionary."""
        assert isinstance(d,dict)
        for k in "problem problems Problem Problems "\
            "solution solutions Solution Solutions "\
            "criterion criteria Criterion Criteria".split(' '):
            assert k in d

        self.comments[1] = d
            

    def mkuuid(self):
        """ Normally called only by __init__().  Can also be called
            to create a unique id for copied instances.
        """
        self.uuid = uuid.uuid1()
        
    def getLabel(self,nchars=32):
        """ Return first nchars of first non-blank line of text"""
        return mkLabel(self.text)
                   
    def updateText(self,text):
        """
        Supports updating a problem's description.
        """
        if isEmptyHtml(text):    
            return                              # prevent empty items
        if self.text != text:
            self.text = text
            self.lastmodified = time.time()     # used to detect that the 
                                                # solution description has 
                                                # changed.        
       
    def addTeamMember(self, id):
        """
        Assumes id has already been validated. Action is idempotent by design.
        """
        if id not in self.lastresponses:
            self.lastresponses[id] = self.lastmodified - 1000.  # To indicate 
                                                # member has not responded yet
    
      
    def delTeamMember(self,id):
        """ 
        Remove a team member from the problem and delete all his/her
        responses.  Action is idempotent by design. 
        """
        if id in self.lastresponses:
            self.lastresponses.pop(id)
            for cid,c in self.criteria.iteritems():
                try:
                    c.responses.pop(id)
                    c.normalizedresponses.pop(id)
                    c.deletionvotes.pop(id)
                except KeyError:
                    pass                        # no response stored
            for sid,s in self.solutions.iteritems():
                try:
                    s.responses.pop(id)
                    s.deletionvotes.pop(id)
                except KeyError:
                    pass                    
                        
            
    def addSolution(self, solution):
        """ Adds a new proposed solution to the problem set. """
        assert isinstance(solution,Solution)
        id = "solution%d"%self.nextsolutionid   # Longer and possibly slower 
                                                # than an int id but directly 
                                                # usable for html element id.
        self.solutions[id] = solution
        self.nextsolutionid += 1 
        self.lastmodified = time.time()       
 
    def delSolution(self,id):
        """ Remove a solution and all responses """
        try:
            self.solutions.pop(id)              # Responses are contained 
                                                # in the solution obj
            self.lastmodified = time.time()
        except KeyError:
            pass    
                        
               
    def addCriterion(self, criterion):
        """ Adds a new criterion to the problem set """
        assert isinstance(criterion,Criterion)
        id = "criterion%d"%self.nextcriterionid # Longer and possibly slower 
                                                # than an int id but directly 
                                                # usable for html element id.
        self.criteria[id] = criterion
        self.nextcriterionid += 1
        self.lastmodified = self.lastnormalized = time.time()

        
    def delCriterion(self,id):
        """ Remove a solution and all responses """
        try:
            self.criteria.pop(id)               # Responses are contained 
                                                # in the solution obj
            self.lastmodified = time.time()
        except KeyError:
            pass    
            
        
    def acceptResponses(self, memberid, criteriaresponses, 
                              solutionresponses, 
                              cdeletionvotes, sdeletionvotes):
        """ 
        Records a team member's responses to the current set of 
        criteria and solutions.  
        """
        assert isinstance(criteriaresponses,dict)
        assert isinstance(solutionresponses,dict)
        assert isinstance(sdeletionvotes,dict)
        assert isinstance(cdeletionvotes,dict)
        if memberid in self.lastresponses.keys():
            for id,value in criteriaresponses.iteritems():
                self.criteria[id].addResponse(memberid,value)
            for id,values in solutionresponses.iteritems():
                #print values
                self.solutions[id].addResponse(memberid,values) 
            for id,value in sdeletionvotes.iteritems():
                self.solutions[id].deletionvotes[memberid] = value
            for id,value in cdeletionvotes.iteritems():
                self.criteria[id].deletionvotes[memberid] = value

            self.lastresponses[memberid] = time.time()  # update last response 
                                                # timestamp
    
    def computeNormalizedCriteria(self):
        """
        For each user, compute and store normalized criteria such that the sum
        of the criteria is 1.0.  This ensures that each user's responses have
        the same total weight.  
        """

        for m in self.lastresponses.keys():
            sum = 0.0
            for id,c in self.criteria.iteritems():
                if None != c.getResponse(m):
                    sum += c.responses[m]
                
            for id,c in self.criteria.iteritems():
                if None != c.getResponse(m):
                    #debug("%d %f %d"%(m,sum,len(self.criteria)))
                    c.normalizeResponse(m,sum,len(self.criteria))    
   
                
                
    def computeCurrentEvaluations(self):
        """ 
        Collect all current criteria responses from teammmembers.  "current"
        means that the lastresponses response is newer than the last change to
        the problem's text including solutions and criteria texts.
        """
        ## 'rv' at end of var names means "response values"
        criteriarv = dict()     
        criteriarvraw= dict()     
        solutionrv = dict()
        scrv = dict()                           # per-criterion responses 
                                                # to solutions.
        wscrv = dict()                          # weighted ...

        self.computeNormalizedCriteria()
        
        for cid,c in self.criteria.iteritems():
            criteriarv[cid] = []
            for m in self.lastresponses.keys():
                #if self.lastresponses[m] > self.lastmodified:
                if None != c.getResponse(m):
                #if self.lastresponses[m] > c.lastmodified:
                    #criteriarv[cid].append(c.getResponse(m)) 
#                    criteriarvraw[cid].append(c.responses[m]) 
                    criteriarv[cid].append(c.normalizedresponses[m]) 
                        
         
        self.crankings = ranking(criteriarv)
        nmax = 0                                # maximum of response counts.
                                                # Used for scaling charts.        
        for sid,s in self.solutions.iteritems():
            solutionrv[sid] = []
            scrv[sid] = dict()
            wscrv[sid] = dict()
            for cid,c in self.criteria.iteritems():
                scrv[sid][cid] = []
                wscrv[sid][cid] = []
            n = 0
            for m in self.lastresponses.keys():
                #if self.lastresponses[m] > self.lastmodified:
                if None != s.getResponse(m):
                #if self.lastresponses[m] > s.lastmodified:
                    n += 1
                    wresponse = 0.0
                    for cid,c in self.criteria.iteritems():
                        if self.lastresponses[m] > c.lastmodified:
                            cresponse = s.responses[m][cid]
                            scrv[sid][cid].append(cresponse)
                            cnr = cresponse*c.normalizedresponses[m]
                            wscrv[sid][cid].append(cnr)
                            wresponse += cnr

                    solutionrv[sid].append(wresponse)  
                nmax = max(nmax,n)

        self.srankings = ranking(solutionrv)
        self.scrankings = dict()
        for sid in solutionrv.keys():
            self.scrankings[sid] = ranking(scrv[sid]) 
                               
        return [criteriarv, solutionrv, scrv, wscrv, nmax]
        
    def responseArray(self,randomids=True):
        """ 
        Returns a 2-D array (list of lists) containing current response
        data.  If randomids is true, the user ids are disguised. The
        randomization is different each time the function is called.
        The output array contains all the response data in a form
        suitable for export as a flat csv file organized in 7 sections:

            1. Solution responses
            2. Criteria responses
            3. Solution Deletion Votes
            4. Criterion Deletion Votes
            5. Solution Timestamps (last activity)
            6. Criterion Timestamps (last activity)
            7. User Response Timestamps (last activity)

        The Criteria response section shows the users' response in raw
        and normalized form. Similarly, the Solution response section
        shows user responses in raw and criterion-weighted forms. 

        """
        from random import shuffle
        lut = dict()                            # user id lookup table
        ids = [i for i in self.lastresponses]   # entire team
        shuffle(ids)                            # in-place
        fakeids = range(len(self.lastresponses))
        shuffle(fakeids)
        for n,i in enumerate(ids):
            if randomids:
                lut[i] = fakeids[n]
            else:
                lut[i] = i                      # not disguised
        
        def tf2n(boolval):                      # True/False --> 0/1
            return 1 if boolval else 0

        self.computeNormalizedCriteria()        # make sure its up to date

        def sget(u,s,cid): 
            """
            User u's response for  solution s to  criterion c.  Returns
            none if user hasn't  responded yet.
            """
            sr = s.responses
            weights = self.criteria[cid].normalizedresponses
            if isinstance(sr.get(u),dict):
                raw = sr[u].get(cid)
                w = weights.get(u)
                weighted = raw*weights.get(u) if w else None
                return raw,weighted
            else:
                return None,None

        def cget(u,cid):
            c = self.criteria[cid]
            raw = c.responses.get(u)
            normal = c.normalizedresponses.get(u)
            return raw,normal

        arr = [[self.getLabel()]]               # init the output array

        def sectionhdr(s,*cols):
            hdr = [s]                           # section label
            hdr.extend(cols)                    # column labels
            arr.extend([
            [""],                               # blank row
            hdr,                                # header row
            ])   

        sectionhdr("Solution Responses",
            "solution","criterion","user",
            "raw", "weighted")    
        for sid,s in self.solutions.iteritems():
            arr.append(["", mkLabel(s.text,64),])
            for cid,c in self.criteria.iteritems():
                arr.append(["","", mkLabel(c.text,64)])
                for u in ids:
                    r,w = sget(u,s,cid)        # floats in -1 ... 1 or None
                    arr.append(["","","",
                        lut[u],                 # user
                        r or "",                # raw
                        w or "",                # weighted
                        ]) 

        sectionhdr("Criteria Responses",
            "criterion","user","raw", "normalized")
        for cid,c in self.criteria.iteritems():
            arr.append(["", mkLabel(c.text,64),])
            for u in ids:
                raw,normal = cget(u,cid)
                arr.append(["","",
                    lut[u],                     # user
                    raw or "",    
                    normal or "", 
                    ])

        sectionhdr("Solution Deletion Votes","solution","user","vote")
        for sid,s in self.solutions.iteritems():
            arr.append(["", mkLabel(s.text,64),])
            for u in ids:
                if s.deletionvotes.get(u) != None:
                    arr.append(["","", lut[u],tf2n(s.deletionvotes[u])])

        sectionhdr("Criterion Deletion Votes","criterion","user","vote")
        for cid,c in self.criteria.iteritems():
            arr.append(["", mkLabel(c.text,64), c.lastmodified])
            for u in ids:
                if c.deletionvotes.get(u) != None:
                    arr.append(["","", lut[u],tf2n(c.deletionvotes[u])])

        sectionhdr("Solution Time Stamps", "solution", "timestamp")
        for sid,s in self.solutions.iteritems():
            arr.append(["", mkLabel(s.text,64), s.lastmodified,])

        sectionhdr("Criteria Time Stamps", "criterion", "timestamp")
        for cid,c in self.criteria.iteritems():
            arr.append(["", mkLabel(c.text,64), c.lastmodified])

        sectionhdr("User Response Time Stamps", "user", "timestamp")    
        for u in ids:
            arr.append(["",lut[u],self.lastresponses[u]])
        return arr


    def getLastResponses(self,memberid):
        """ 
        Return results of last responses by this member to criteria and
        solutions that are unchanged since the time of the response.  
        """
        changedcriteria = []
        criteriaresponses = dict()
        cdeletionvotes = dict()
        for cid,c in self.criteria.iteritems():
            if c.lastmodified < self.lastresponses[memberid]:
                criteriaresponses[cid] = c.responses[memberid]
                cdeletionvotes[cid] = c.deletionvotes[memberid]    
            else:
                criteriaresponses[cid] = 0      # no weight assigned
                changedcriteria.append(cid)
                
        changedsolutions = []
        solutionresponses = dict()
        sdeletionvotes = dict()
        for sid,s in self.solutions.iteritems():
            responses = []
            if s.lastmodified < self.lastresponses[memberid]:
                sdeletionvotes[sid] = s.deletionvotes[memberid]
                for cid,c in self.criteria.iteritems():
                    if not cid in changedcriteria:
                        responses.append([cid,s.responses[memberid][cid]])
                    else:
                        responses.append([cid,0])
            else:
                for cid,c in self.criteria.iteritems():
                    responses.append([cid,0])

                changedsolutions.append(sid)
                
            solutionresponses[sid] = responses                                 
         
        return ResponseRecord(criteriaresponses,
            solutionresponses,
            changedcriteria,
            changedsolutions,
            cdeletionvotes,
            sdeletionvotes,
            )       

    def sortSolutionsReverseChrono(self):
        """ 
        Returns a list of (id,solution) pairs sorted by last modification time,
        latest to earliest
        """
        slist = [i for i in self.solutions.iteritems()]
        slist.sort(lambda x,y : -1 if x[1].lastmodified > y[1].lastmodified \
                                else int(x[1].lastmodified < y[1].lastmodified))
        return slist
                                                                           
    def sortCriteriaReverseChrono(self):
        """ 
        Returns a list of (id,criterion) pairs sorted by last modification
        time, latest to earliest
        """
        clist = [i for i in self.criteria.iteritems()]
        clist.sort(lambda x,y : -1 if x[1].lastmodified > y[1].lastmodified \
                                else int(x[1].lastmodified < y[1].lastmodified))
        return clist
     
    def applyDeletionVotes(self):
        """ 
        Counts deletion votes and removes solutions and criteria that have
        exceeded the threshold set in self.itemdeletionthreshold.
        """

        nmembers = len(self.lastresponses)
        nneeded  = int(round(nmembers*self.itemdeletionthreshold))
        sdels = [] 
        cdels = []
        for sid,s in self.solutions.iteritems():
            diter = s.deletionvotes.iteritems()
            ndel = sum([1 if v == True else 0 for k,v in diter])
            if ndel >= nneeded:
                sdels.append(sid)

        for sid in sdels:
            self.delSolution(sid)
    
        for cid,c in self.criteria.iteritems():
            diter = c.deletionvotes.iteritems()
            ndel = sum([1 if v == True else 0 for k,v in diter])
            if ndel >= nneeded:
                cdels.append(cid)

        for cid in cdels:
            self.delCriterion(cid)
        

    def tallyDeletionVotes(self):
        """ Returns a dictionary structured as:
                "cvotes": {cid:nyes, ...}
                "svotes": {sid:nyes, ...}
                "nmembers": nmembers
        """
        d = {"cvotes":{}, "svotes":{}, "nmembers":None}
        for sid,s in self.solutions.iteritems():
            d["svotes"][sid] = sum([1 if v == True else 0 
                                    for k,v in s.deletionvotes.iteritems()])

        for cid,c in self.criteria.iteritems():
            d["cvotes"][cid] = sum([1 if v == True else 0 
                                for k,v in c.deletionvotes.iteritems()])
        
        d["nmembers"] = len(self.lastresponses)

        return d                                                                      

            
class ResponseRecord(object):
    """ 
    Holds a record of a team member's most recent response to a Problem.
    Solutions and criteria that have been edited since the member's last
    response are set to default values and lists of the ids of each changed
    items are available.
    """
    def __init__(self,criteriaresponses,solutionresponses,
                        changedcriteria,changedsolutions,
                        cdeletionvotes, sdeletionvotes):
        self.__dict__.update(locals())

def cloneProblem(p,newcreatorid):
    """ 
    Creates a clone of an existing problem, p. 
    """
    assert isinstance(p,Problem)
    q = deepcopy(p)
    participants = q.lastresponses.keys()
    for m in participants:                   # remove participants
        q.delTeamMember(m)
    q.creatorid = newcreatorid
    q.addTeamMember(newcreatorid)
    q.mkuuid()
    q.updateText( "CLONED : "+p.text)
    return q


def mergeProblem(p1,p2):
    """
    Merges added solutions and criteria from problem p1 to problem p2.
    """
    #assert isinstance(p1,Problem)
    #assert isinstance(p2,Problem)
    assert p1.uuid == p2.uuid                   # enforce a design assumption
    p2suuids = [s.uuid for s in p2.solutions.values()] 
    for s in p1.solutions.values():
        if s.uuid not in p2suuids:
            p2.addSolution(s)
        
    p2cuuids = [c.uuid for c in p2.criteria.values()]
    for c in p1.criteria.values():
        if c.uuid not in p2cuuids:
           p2.addCriterion(c)
    
    ## add all team members from p1.
    ## the addTeamMember() method ignores any
    ## that are already there.
    for id in p1.lastresponses:
        p2.addTeamMember(id)

 
def mergeResponses(userid,p1,p2):
    """ 
    Add new responses from a single user in p1 to p2. 
    """
    #assert isinstance(p1,Problem)
    #assert isinstance(p2,Problem)
    assert p1.uuid == p2.uuid                   ## enforce a design assumption
    
    r = p1.getLastResponses(userid)             ## r is a ResponseRecord
    #debug("changed criteria: %s\n"%repr(r.changedcriteria))
    #debug("changed solutions: %s\n"%repr(r.changedsolutions))
    #enforce design assumption that solutions and criteria
    #are never edited.
    assert(len(r.changedcriteria) == 0)
    assert(len(r.changedsolutions) == 0) 
    
    p2.acceptResponses(userid,r.criteriaresponses, r.solutionresponses, 
                        r.cdeletionvotes, r.sdeletionvotes)

    ## finally, copy any new comments to p2. We use dictionary updates
    ## for this operation since the comments are keyed by timestamp, hence
    ## no (practical) likelihood of overwriting another user's comments.
    getComments(p2).update(getComments(p1))
    for k,s in p1.solutions.iteritems():
        if k in p2.solutions:
            getComments(p2.solutions[k]).update(getComments(s))
    for k,c in p1.criteria.iteritems():
        if k in p2.criteria:
            getComments(p2.criteria[k]).update(getComments(c))

    

